package com.vois.jack.btmgr.blebase;

import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.util.Log;

import com.vois.jack.btmgr.common.State;
import com.vois.jack.btmgr.common.StateMachine;
import com.vois.jack.btmgr.util.Logger;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Pattern;

public class BleDevMgr extends StateMachine implements BleDevice.BleDeviceCloseListener{
    private Logger logger = Logger.getLogger(BleDevMgr.class);
    final static private String EXTRA_NAME_FILTER_LIST  = "BleDevMgr.ble_name_filters";
    final static private String EXTRA_MAC_FILTER_LIST   = "BleDevMgr.ble_mac_filters";
    final static private String EXTRA_SCAN_TIMEOUT      = "BleDevMgr.ble_scan_timeout";
    final static private String EXTRA_BLUETOOTH_DEVICE  = "BleDevMgr.ble_device";
    final static private String EXTRA_SCAN_DATA         = "BleDevMgr.scan_data";
    final static private String EXTRA_BLUETOOTH_NAME    = "BleDevMgr.bluetooth_name";

    private Map<String, BleDevice> bleDeviceMap;
    private BleMgrCallback callback;

    private InitState initState;
    private BluetoothOnState bluetoothOnState;
    private BluetoothOffState bluetoothOffState;
    private boolean isStarted;
    private boolean isStarting;
    private Context context;
    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
                logger.d( "onReceive: state:" + state);
                if (isStarted) {
                    sendMessage(BleFsmMsg.BLE_FSM_BLUETOOTH_STATE_CHANGED_MSG.ordinal(), state);
                }
            }
        }
    };

    public interface BleMgrCallback {
        void onStarted();
        void onStopped();
        void onBluetoothOn();
        void onBluetoothOff();
    }

    public interface BleScanCallback {
        void onStarting();
        void onScanFound(BluetoothDevice device, String name, byte[] rawScanData, int rssi);
        void onBatchScanResultFound(BleScanResult[] scanResults);
        void onScanFail();
        void onScanStopped();
        void onScanTimeout();
    }

    class InitState extends State {
        @Override
        public void enter() {
            logger.d( "enter: InitState");

            if (BleHelper.getInstance().isBluetoothOn()) {
                transitionTo(bluetoothOnState);
            }else {
                transitionTo(bluetoothOffState);
            }

            isStarted = true;
            isStarting = false;
            if (callback != null) {
                callback.onStarted();
            }
        }

        @Override
        public void exit() {
            logger.d( "exit: InitState");
            Set<String> keys = bleDeviceMap.keySet();
            for (String key: keys) {
                BleDevice bleDevice = bleDeviceMap.get(key);
                if (bleDevice != null) {
                    bleDevice.closeBleDevice();
                }
            }

            bleDeviceMap.clear();
        }
    }

    class BluetoothOnState extends State {
        final static private String EXTRA_LIST_SCAN_DATA    = "ble_scan_data_list";
        final static private String EXTRA_RAW_SCAN_DATA     = "ble_scan_raw_data";
        final static private String EXTRA_SCAN_DEVICE_NAME  = "ble_scan_device_name";
        final static private String EXTRA_SCAN_RSSI         = "ble_scan_rssi";
        Object scanCallbackHolder = null;
        BleScanCallback scanCallback = null;
        Timer scanTimer = null;
        boolean isScanning = false;

        @TargetApi(Build.VERSION_CODES.M)
        private void startNewBleScan(String[] nameFilters, String[] macFilters) {
            final String[] namePatternFilters = nameFilters;
            final String[] internalMacFilters = macFilters;
            logger.d( "startNewBleScan: ");
            ScanCallback scanCallback = new ScanCallback() {
                @Override
                public void onScanResult(int callbackType, ScanResult result) {
                    if (result.getScanRecord() != null) {
                        String name = result.getScanRecord().getDeviceName();
                        if (name == null) {
                            name = result.getDevice().getName();
                        }

                        logger.d("Scan name: " + name);
                        if (name != null) {
                            boolean match = (namePatternFilters == null);

                            if (internalMacFilters == null) {
                                if (namePatternFilters != null) {
                                    for (String scanNameFilter : namePatternFilters) {
                                        match = Pattern.matches(scanNameFilter, name);
                                        if (match) {
                                            logger.d("found device: " + name);
                                            break;
                                        }
                                    }
                                }
                            } else {
                                match = true;
                            }

                            if (match) {
                                Bundle bundle = new Bundle();
                                bundle.putByteArray(EXTRA_RAW_SCAN_DATA, result.getScanRecord().getBytes());
                                bundle.putString(EXTRA_SCAN_DEVICE_NAME, name);
                                bundle.putInt(EXTRA_SCAN_RSSI, result.getRssi());
                                Message message = obtainMessage(BleFsmMsg.BLE_FSM_SCAN_FOUND_MSG.ordinal(), result.getDevice());
                                message.setData(bundle);
                                sendMessage(message);
                            }
                        }
                    }
                }

                @Override
                public void onBatchScanResults(List<ScanResult> results) {
                    logger.d( "onBatchScanResults: " + results.size());
                    Bundle bundle = new Bundle();
                    BleScanResult[] foundResult = new BleScanResult[results.size()];
                    int i = 0;

                    for(ScanResult result : results) {
                        ScanRecord scanRecord = result.getScanRecord();
                        if (scanRecord != null) {
                            foundResult[i++] = new BleScanResult(result.getScanRecord().getBytes(), result.getDevice(), result.getRssi());
                        }else {
                            foundResult[i++] = new BleScanResult(null, result.getDevice(), result.getRssi());
                        }
                    }
                    bundle.putParcelableArray(EXTRA_LIST_SCAN_DATA, foundResult);
                    Message message = obtainMessage(BleFsmMsg.BLE_FSM_SCAN_FOUND_BATCHRESULT_MSG.ordinal());
                    message.setData(bundle);
                    sendMessage(message);
                }

                @Override
                public void onScanFailed(int errorCode) {
                    logger.d( "onScanFailed: errorCode:" + errorCode);
                    sendMessage(BleFsmMsg.BLE_FSM_SCAN_FAILED_MSG.ordinal(), errorCode);
                }
            };

            List<ScanFilter> list = new ArrayList<>();
            if(macFilters != null && macFilters.length > 0) {
                logger.d( "startNewBleScan: mac filters");
                for (String filter: macFilters) {
                    logger.d( "macfilter: " + filter);
                    ScanFilter scanFilter = new ScanFilter.Builder().setDeviceAddress(filter).build();
                    list.add(scanFilter);
                }
            }

            if(list.size() == 0) {
                logger.d( "startNewBleScan: list size = 0");
                BleHelper.getInstance().startScanLeBluetooth(null, ScanSettings.CALLBACK_TYPE_ALL_MATCHES, scanCallback, ScanSettings.SCAN_MODE_LOW_LATENCY);
            }else{
                logger.d( "startNewBleScan: list size is not 0");
                BleHelper.getInstance().startScanLeBluetooth(list, ScanSettings.CALLBACK_TYPE_ALL_MATCHES, scanCallback, ScanSettings.SCAN_MODE_LOW_LATENCY);
            }

            scanCallbackHolder = scanCallback;
        }

        @TargetApi(Build.VERSION_CODES.M)
        private void stopNewBleScan() {
            logger.d( "stopNewBleScan");
            if(scanCallbackHolder != null) {
                ScanCallback scanCallback = (ScanCallback) scanCallbackHolder;
                BleHelper.getInstance().stopScanLeBluetooth(scanCallback);
            }
        }

        @TargetApi(Build.VERSION_CODES.KITKAT)
        private void startOldBleScan(final String[] nameFilters, final String[] macFilters) {
            logger.d( "startOldBleScan");
            BluetoothAdapter.LeScanCallback scanCallback = new BluetoothAdapter.LeScanCallback() {
                @Override
                public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
                    boolean found = false;
                    String bleName;
                    BleScanRecord bleScanRecord = BleScanRecord.parseFromBytes(scanRecord);
                    if (bleScanRecord != null) {
                        bleName = bleScanRecord.getDeviceName();
                    }else {
                        bleName = device.getName();
                    }

                    logger.d("Scan name: " + bleName);

                    if (nameFilters != null || macFilters != null) {
                        if (nameFilters != null && bleName != null) {
                            for (String scanNameFilter : nameFilters) {
                                found = Pattern.matches(scanNameFilter, bleName);
                                if (found) {
                                    break;
                                }
                            }
                        }

                        if (macFilters != null) {
                            for (String filter : macFilters) {
                                if (device.getAddress().equals(filter)) {
                                    found = true;
                                    break;
                                }
                            }
                        }
                    }else {
                        found = true;
                    }

                    if(found) {
                        logger.d( "onLeScan: " + ((device.getName() != null) ? device.getName():device.getAddress()));
                        Bundle bundle = new Bundle();
                        bundle.putByteArray(EXTRA_RAW_SCAN_DATA, scanRecord);
                        bundle.putString(EXTRA_SCAN_DEVICE_NAME, bleName);
                        bundle.putInt(EXTRA_SCAN_RSSI, rssi);
                        Message message = obtainMessage(BleFsmMsg.BLE_FSM_SCAN_FOUND_MSG.ordinal(), device);
                        message.setData(bundle);
                        sendMessage(message);
                    }
                }
            };

            BleHelper.getInstance().startScanLeBluetooth(scanCallback);
            scanCallbackHolder = scanCallback;
        }

        @TargetApi(Build.VERSION_CODES.KITKAT)
        private void stopOldBleScan() {
            logger.d( "stopOldBleScan");
            if(scanCallbackHolder != null) {
                BluetoothAdapter.LeScanCallback scanCallback = (BluetoothAdapter.LeScanCallback) scanCallbackHolder;
                BleHelper.getInstance().stopScanLeBluetooth(scanCallback);
            }
        }

        @Override
        public void enter() {
            logger.d( "enter: BluetoothOnState");
            for (BleDevice bleDevice: bleDeviceMap.values()) {
                bleDevice.openBleDevice(true);
            }

            if (callback != null) {
                callback.onBluetoothOn();
            }
        }

        @Override
        public void exit() {
            logger.d( "exit: BluetoothOnState");
            for (BleDevice bleDevice: bleDeviceMap.values()) {
                bleDevice.disconnectBleDevice();
            }

            if (scanTimer != null) {
                scanTimer.cancel();
                scanTimer = null;
            }

            if (isScanning) {
                if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    stopNewBleScan();
                }else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    stopOldBleScan();
                }

                if (scanCallback != null) {
                    scanCallback.onScanStopped();
                    scanCallback = null;
                }

                isScanning = false;
            }
        }

        @Override
        public boolean processMessage(Message msg) {
            boolean ret = false;
            if (msg.what < 0) {
                return ret;
            }
            BleFsmMsg fsmMsg = BleFsmMsg.values()[msg.what];
            logger.d( "processMessage: BluetoothOnState:" + fsmMsg);

            switch (fsmMsg) {
                case BLE_FSM_BLUETOOTH_STATE_CHANGED_MSG: {
                    int state = msg.arg1;

                    if (state == BluetoothAdapter.STATE_OFF) {
                        transitionTo(bluetoothOffState);
                    }

                    ret = true;
                }
                break;

                case BLE_FSM_START_SCAN_MSG: {
                    Bundle data = msg.getData();
                    String[] nameFilters = data.getStringArray(EXTRA_NAME_FILTER_LIST);
                    String[] macFilters = data.getStringArray(EXTRA_MAC_FILTER_LIST);
                    int timeout = data.getInt(EXTRA_SCAN_TIMEOUT);
                    scanCallback = (BleScanCallback) msg.obj;

                    if (isScanning) {
                        ret = true;
                        break;
                    }

                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        startNewBleScan(nameFilters, macFilters);
                    }else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                        startOldBleScan(nameFilters, macFilters);
                    }else {
                        isScanning = false;
                        if (scanCallback != null) {
                            scanCallback.onScanFail();
                            scanCallback.onScanStopped();
                            scanCallback = null;
                            ret = true;
                            break;
                        }
                    }

                    isScanning = true;
                    scanTimer = new Timer();
                    scanTimer.schedule(new TimerTask() {
                        @Override
                        public void run() {
                            sendMessage(BleFsmMsg.BLE_FSM_SCAN_TIMEOUT_MSG.ordinal());
                        }
                    }, timeout);

                    if (scanCallback != null) {
                        scanCallback.onStarting();
                    }

                    ret = true;
                }
                break;
                case BLE_FSM_SCAN_TIMEOUT_MSG:
                case BLE_FSM_STOP_SCAN_MSG: {
                    if (scanTimer != null) {
                        scanTimer.cancel();
                        scanTimer = null;
                    }

                    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        stopNewBleScan();
                    }else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                        stopOldBleScan();
                    }else {
                        if (scanCallback != null) {
                            scanCallback.onScanFail();
                        }
                    }

                    if (scanCallback != null) {
                        if (fsmMsg == BleFsmMsg.BLE_FSM_SCAN_TIMEOUT_MSG) {
                            scanCallback.onScanTimeout();
                        }else {
                            scanCallback.onScanStopped();
                        }
                    }

                    isScanning = false;
                    scanCallback = null;
                    ret = true;
                }
                break;

                case BLE_FSM_SCAN_FOUND_MSG: {
                    if (isScanning) {
                        Bundle data = msg.getData();
                        BluetoothDevice device = (BluetoothDevice) msg.obj;
                        byte[] rawBytes = data.getByteArray(EXTRA_RAW_SCAN_DATA);
                        String bleName = data.getString(EXTRA_SCAN_DEVICE_NAME);
                        int rssi = data.getInt(EXTRA_SCAN_RSSI);

                        if (scanCallback != null && bleName != null) {
                            scanCallback.onScanFound(device, bleName, rawBytes, rssi);
                        }
                    }

                    ret = true;
                }
                break;

                case BLE_FSM_SCAN_FOUND_BATCHRESULT_MSG: {
                    if (isScanning) {
                        Bundle data = msg.getData();
                        BleScanResult[] scanResults = (BleScanResult[]) data.getParcelableArray(EXTRA_LIST_SCAN_DATA);
                        if (scanCallback != null) {
                            scanCallback.onBatchScanResultFound(scanResults);
                        }
                    }
                }
                break;

                case BLE_FSM_SCAN_FAILED_MSG: {
                    if (scanCallback != null) {
                        scanCallback.onScanFail();
                    }
                    sendMessage(BleFsmMsg.BLE_FSM_STOP_SCAN_MSG.ordinal());

                    ret = true;
                }
                break;

                case BLE_FSM_OPEN_BLE_DEVICE_MSG: {
                    Bundle data = msg.getData();
                    BluetoothDevice device = data.getParcelable(EXTRA_BLUETOOTH_DEVICE);
                    byte[] scanData = data.getByteArray(EXTRA_SCAN_DATA);
                    String name = data.getString(EXTRA_BLUETOOTH_NAME);
                    BleDevice.BleDeviceListener listener = (BleDevice.BleDeviceListener) msg.obj;

                    logger.d("BLE_FSM_OPEN_BLE_DEVICE_MSG name:" + name);
                    if (!bleDeviceMap.containsKey(device.getAddress())) {
                        BleDevice bleDevice = BleDeviceBuilder.buildBleDevice(device, name);
                        logger.d( "processMessage: " + bleDevice.getDeviceModel());
                        bleDevice.setName(name);
                        bleDevice.setListener(listener);
                        bleDevice.setCloseListener(BleDevMgr.this);
                        bleDevice.setScanRecord(scanData);
                        bleDeviceMap.put(device.getAddress(), bleDevice);
                    }else {
                        BleDevice bleDevice = bleDeviceMap.get(device.getAddress());
                        bleDevice.setListener(listener);
                        bleDevice.setScanRecord(scanData);
                        if (!bleDevice.isWorking()) {
                            bleDevice.openBleDevice(false);
                        }
                    }

                    ret = true;
                }
                break;

                case BLE_FSM_CLOSE_BLE_DEVICE_MSG: {
                    String mac = (String) msg.obj;

                    if (bleDeviceMap != null && bleDeviceMap.containsKey(mac)) {
                        BleDevice bleDevice = bleDeviceMap.get(mac);
                        if (bleDevice != null) {
                            bleDevice.closeBleDevice();
                        }
                    }

                    ret = true;
                }
                break;

                case BLE_FSM_REMOVE_BLE_DEVICE_MSG: {
                    String mac = (String) msg.obj;
                    if (bleDeviceMap != null && bleDeviceMap.containsKey(mac)) {
                        bleDeviceMap.remove(mac);
                    }

                    ret = true;
                }
                break;
            }

            return ret;
        }
    }

    class BluetoothOffState extends State {
        @Override
        public void enter() {
            logger.d( "enter: BluetoothOffState");
            if (callback != null) {
                callback.onBluetoothOff();
            }
        }

        @Override
        public boolean processMessage(Message msg) {
            boolean ret = false;
            if (msg.what < 0) {
                return ret;
            }
            BleFsmMsg fsmMsg = BleFsmMsg.values()[msg.what];
            logger.d( "processMessage: BluetoothOffState:" + fsmMsg);

            switch (fsmMsg) {
                case BLE_FSM_BLUETOOTH_STATE_CHANGED_MSG: {
                    int state = msg.arg1;
                    if (state == BluetoothAdapter.STATE_ON) {
                        transitionTo(bluetoothOnState);
                    }
                }
                break;
            }

            return ret;
        }
    }

    public BleDevMgr(Context context, BleMgrCallback callback) {
        super("BLE_MGR_FSM");
        this.context = context;
        BleHelper.getInstance().init(context);
        bleDeviceMap = new HashMap<>();
        this.callback = callback;
        initState = new InitState();
        bluetoothOnState = new BluetoothOnState();
        bluetoothOffState = new BluetoothOffState();
        isStarting = false;
        isStarted = false;

        addState(initState);
        addState(bluetoothOnState, initState);
        addState(bluetoothOffState, initState);

        setInitialState(initState);
    }

    public void startBleManager() {
        if (!isStarting && !isStarted) {
            IntentFilter intentFilter = new IntentFilter();
            intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
            context.registerReceiver(receiver, intentFilter);
            start();
            isStarting = true;
        }
    }

    public void stopBleManager() {
        if (isStarted) {
            quitNow();
            isStarted = false;
            context.unregisterReceiver(receiver);
            context = null;
        }
    }

    @Override
    protected void onQuitting() {
        logger.d( "onQuitting");
        if (callback != null) {
            callback.onStopped();
        }
    }

    public void startScanBleDevice(String[] names, String[] macs, int timeout, BleScanCallback bleScanCallback) {
        if (isStarted) {
            Bundle data = new Bundle();
            data.putStringArray(EXTRA_NAME_FILTER_LIST, names);
            data.putStringArray(EXTRA_MAC_FILTER_LIST, macs);
            data.putInt(EXTRA_SCAN_TIMEOUT, timeout);

            Message message = obtainMessage(BleFsmMsg.BLE_FSM_START_SCAN_MSG.ordinal(), bleScanCallback);
            message.setData(data);
            sendMessage(message);
        }
    }

    public void stopScanBleDevice() {
        if (isStarted) {
            sendMessageAtFrontOfQueue(obtainMessage(BleFsmMsg.BLE_FSM_STOP_SCAN_MSG.ordinal()));
        }
    }

    public void openBleDevice(BluetoothDevice bluetoothDevice, String bleName, byte[] scanData, BleDevice.BleDeviceListener listener) {
        if (isStarted) {
            Bundle data = new Bundle();
            data.putParcelable(EXTRA_BLUETOOTH_DEVICE, bluetoothDevice);
            data.putString(EXTRA_BLUETOOTH_NAME, bleName);
            data.putByteArray(EXTRA_SCAN_DATA, scanData);
            Message message = obtainMessage(BleFsmMsg.BLE_FSM_OPEN_BLE_DEVICE_MSG.ordinal(), listener);
            message.setData(data);
            sendMessage(message);
        }
    }

    public void closeBleDevice(BluetoothDevice bluetoothDevice) {
        closeBleDevice(bluetoothDevice.getAddress());
    }

    public void closeBleDevice(String mac) {
        if (isStarted) {
            sendMessage(BleFsmMsg.BLE_FSM_CLOSE_BLE_DEVICE_MSG.ordinal(), mac);
        }
    }

    public void connectBleDevice(String mac, byte[] rawScanData) {
        if (bleDeviceMap.containsKey(mac)) {
            BleDevice bleDevice = bleDeviceMap.get(mac);
            if (bleDevice != null && !bleDevice.isConnected()) {
                bleDevice.setScanRecord(rawScanData);
                bleDevice.connectBleDevice();
            }
        }
    }

    public void disconnectBleDevice(String mac) {
        if (bleDeviceMap.containsKey(mac)) {
            BleDevice bleDevice = bleDeviceMap.get(mac);
            if (bleDevice != null && bleDevice.isConnected()) {
                bleDevice.disconnectBleDevice();
            }
        }
    }

    @Override
    public void onBleDeviceClosed(BleDevice bleDevice) {
        logger.d("onBleDeviceClosed:" + bleDevice.getName());
        sendMessage(BleFsmMsg.BLE_FSM_REMOVE_BLE_DEVICE_MSG.ordinal(), bleDevice.getMac());
    }

    public BleDevice getBleDevice(BluetoothDevice device) {
        return getBleDevice(device.getAddress());
    }

    public BleDevice getBleDevice(String address) {
        if (address != null) {
            if (bleDeviceMap.containsKey(address)) {
                return bleDeviceMap.get(address);
            }
        }

        return null;
    }


    public boolean isBleDeviceConnected(BluetoothDevice device) {
        if (device != null) {
            if (bleDeviceMap.containsKey(device.getAddress())) {
                BleDevice bleDevice = bleDeviceMap.get(device.getAddress());
                if (bleDevice != null) {
                    return bleDevice.isConnected();
                }
            }
        }

        return false;
    }

    public List<BleDevice> getConnectedBleDevice() {
        List<BleDevice> list = new ArrayList<>();
        for (BleDevice bleDevice: bleDeviceMap.values()) {
            if (bleDevice.isConnected()) {
                list.add(bleDevice);
            }
        }

        return list;
    }

    public boolean isScanProcessing() {
        return BleHelper.getInstance().isScanProcessing();
    }
}
